///////////////////////////////////////////////////////////////////////////////
//
//  The contents of this file are subject to the Mozilla Public License
//  Version 1.1 (the "License"); you may not use this file except in
//  compliance with the License. You may obtain a copy of the License at
//  http://www.mozilla.org/MPL/
//
//  Software distributed under the License is distributed on an "AS IS"
//  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//  License for the specific language governing rights and limitations
//  under the License.
//
//  The Original Code is MP4v2.
//
//  The Initial Developer of the Original Code is Ullrich Pollaehne.
//  Portions created by Kona Blend are Copyright (C) 2008.
//  Portions created by David Byron are Copyright (C) 2010.
//  All Rights Reserved.
//
//  Contributors:
//      Kona Blend, kona8lend@@gmail.com
//      Ullrich Pollaehne, u.pollaehne@@gmail.com
//      David Byron, dbyron@dbyron.com
//
///////////////////////////////////////////////////////////////////////////////
#include "util/impl.h"

namespace mp4v2
{
    namespace util
    {

        ///////////////////////////////////////////////////////////////////////////////
        ///
        /// Chapter utility program class.
        ///
        /// This class provides an implementation for a QuickTime/Nero chapter utility which
        /// allows to add, delete, convert export or import QuickTime and Nero chapters
        /// in MP4 container files.
        ///
        ///
        /// @see Utility
        ///
        ///////////////////////////////////////////////////////////////////////////////
        class ChapterUtility : public Utility
        {
        private:
            static const double CHAPTERTIMESCALE; //!< the timescale used for chapter tracks (1000)

            enum FileLongCode
            {
                LC_CHPT_ANY = _LC_MAX,
                LC_CHPT_QT,
                LC_CHPT_NERO,
                LC_CHPT_COMMON,
                LC_CHP_LIST,
                LC_CHP_CONVERT,
                LC_CHP_EVERY,
                LC_CHP_EXPORT,
                LC_CHP_IMPORT,
                LC_CHP_REMOVE
            };

            enum ChapterFormat
            {
                CHPT_FMT_NATIVE,
                CHPT_FMT_COMMON
            };

            enum FormatState
            {
                FMT_STATE_INITIAL,
                FMT_STATE_TIME_LINE,
                FMT_STATE_TITLE_LINE,
                FMT_STATE_FINISH
            };

        public:
            ChapterUtility( int, char ** );

        protected:
            // delegates implementation
            bool utility_option( int, bool & );
            bool utility_job( JobContext & );

        private:
            bool actionList    ( JobContext & );
            bool actionConvert ( JobContext & );
            bool actionEvery   ( JobContext & );
            bool actionExport  ( JobContext & );
            bool actionImport  ( JobContext & );
            bool actionRemove  ( JobContext & );

        private:
            Group  _actionGroup;
            Group  _parmGroup;

            bool        (ChapterUtility::*_action)( JobContext & );
            void        fixQtScale(MP4FileHandle );
            MP4TrackId  getReferencingTrack( MP4FileHandle, bool & );
            string      getChapterTypeName( MP4ChapterType ) const;
            bool        parseChapterFile( const string, vector<MP4Chapter_t> &, Timecode::Format & );
            bool        readChapterFile( const string, char **, File::Size & );
            MP4Duration convertFrameToMillis( MP4Duration, uint32_t );

            MP4ChapterType _ChapterType;
            ChapterFormat  _ChapterFormat;
            uint32_t       _ChaptersEvery;
            string         _ChapterFile;
        };

        ///////////////////////////////////////////////////////////////////////////////

        const double ChapterUtility::CHAPTERTIMESCALE = 1000.0;

        ///////////////////////////////////////////////////////////////////////////////

        ChapterUtility::ChapterUtility( int argc, char **argv )
            : Utility        ( "mp4chaps", argc, argv )
            , _actionGroup   ( "ACTIONS" )
            , _parmGroup     ( "ACTION PARAMETERS" )
            , _action        ( NULL )
            , _ChapterType   ( MP4ChapterTypeAny )
            , _ChapterFormat ( CHPT_FMT_NATIVE )
            , _ChaptersEvery ( 0 )
        {
            // add standard options which make sense for this utility
            _group.add( STD_OPTIMIZE );
            _group.add( STD_DRYRUN );
            _group.add( STD_KEEPGOING );
            _group.add( STD_OVERWRITE );
            _group.add( STD_FORCE );
            _group.add( STD_QUIET );
            _group.add( STD_DEBUG );
            _group.add( STD_VERBOSE );
            _group.add( STD_HELP );
            _group.add( STD_VERSION );
            _group.add( STD_VERSIONX );

            _parmGroup.add( 'A', false, "chapter-any",   false, LC_CHPT_ANY,    "act on any chapter type (default)" );
            _parmGroup.add( 'Q', false, "chapter-qt",    false, LC_CHPT_QT,     "act on QuickTime chapters" );
            _parmGroup.add( 'N', false, "chapter-nero",  false, LC_CHPT_NERO,   "act on Nero chapters" );
            _parmGroup.add( 'C', false, "format-common", false, LC_CHPT_COMMON, "export chapters in common format" );
            _groups.push_back( &_parmGroup );

            _actionGroup.add( 'l', false, "list",    false, LC_CHP_LIST,    "list available chapters" );
            _actionGroup.add( 'c', false, "convert", false, LC_CHP_CONVERT, "convert available chapters" );
            _actionGroup.add( 'e', true,  "every",   true,  LC_CHP_EVERY,   "create chapters every NUM seconds", "NUM" );
            _actionGroup.add( 'x', false, "export",  false, LC_CHP_EXPORT,  "export chapters to mp4file.chapters.txt", "TXT" );
            _actionGroup.add( 'i', false, "import",  false, LC_CHP_IMPORT,  "import chapters from mp4file.chapters.txt", "TXT" );
            _actionGroup.add( 'r', false, "remove",  false, LC_CHP_REMOVE,  "remove all chapters" );
            _groups.push_back( &_actionGroup );

            _usage = "[OPTION]... ACTION [ACTION PARAMETERS] mp4file...";
            _description =
                // 79-cols, inclusive, max desired width
                // |----------------------------------------------------------------------------|
                "\nFor each mp4 file specified, perform the specified ACTION. An action must be"
                "\nspecified. Some options are not applicable to some actions.";
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Action for listing chapters from <b>job.file</b>
         *
         *
         *  @param job the job to process
         *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
         */
        bool
        ChapterUtility::actionList( JobContext &job )
        {
            job.fileHandle = MP4Read( job.file.c_str() );
            if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
            {
                return herrf( "unable to open for read: %s\n", job.file.c_str() );
            }

            MP4Chapter_t *chapters = 0;
            uint32_t chapterCount = 0;

            // get the list of chapters
            MP4ChapterType chtp = MP4GetChapters(job.fileHandle, &chapters, &chapterCount, _ChapterType);
            if (0 == chapterCount)
            {
                verbose1f( "File \"%s\" does not contain chapters of type %s\n", job.file.c_str(),
                           getChapterTypeName( _ChapterType ).c_str() );
                return SUCCESS;
            }

            // start output (more or less like mp4box does)
            ostringstream report;
            report << getChapterTypeName( chtp ) << ' ' << "Chapters of " << '"' << job.file << '"' << endl;

            Timecode duration(0, CHAPTERTIMESCALE);
            duration.setFormat( Timecode::DECIMAL );
            for (uint32_t i = 0; i < chapterCount; ++i)
            {
                // print the infos
                report << '\t' << "Chapter #" << setw( 3 ) << setfill( '0' ) << i + 1
                       << " - " << duration.svalue << " - " << '"' << chapters[i].title << '"' << endl;

                // add the duration of this chapter to the sum (is the start time of the next chapter)
                duration += Timecode(chapters[i].duration, CHAPTERTIMESCALE);
            }

            verbose1f( "%s", report.str().c_str() );

            // free up the memory
            MP4Free(chapters);

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Action for converting chapters in <b>job.file</b>
         *
         *
         *  @param job the job to process
         *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
         */
        bool
        ChapterUtility::actionConvert( JobContext &job )
        {
            MP4ChapterType sourceType;

            switch( _ChapterType )
            {
            case MP4ChapterTypeNero:
                sourceType = MP4ChapterTypeQt;
                break;
            case MP4ChapterTypeQt:
                sourceType = MP4ChapterTypeNero;
                break;
            default:
                return herrf( "invalid chapter type \"%s\" define the chapter type to convert to\n",
                              getChapterTypeName( _ChapterType ).c_str() );
            }

            ostringstream oss;
            oss << "converting chapters in file " << '"' << job.file << '"'
                << " from " << getChapterTypeName( sourceType ) << " to " << getChapterTypeName( _ChapterType ) << endl;

            verbose1f( "%s", oss.str().c_str() );
            if( dryrunAbort() )
            {
                return SUCCESS;
            }

            job.fileHandle = MP4Modify( job.file.c_str() );
            if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
            {
                return herrf( "unable to open for write: %s\n", job.file.c_str() );
            }

            MP4ChapterType chtp = MP4ConvertChapters( job.fileHandle, _ChapterType );
            if( MP4ChapterTypeNone == chtp )
            {
                return herrf( "File %s does not contain chapters of type %s\n", job.file.c_str(),
                              getChapterTypeName( sourceType ).c_str() );
            }

            fixQtScale( job.fileHandle );
            job.optimizeApplicable = true;

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Action for setting chapters every n second in <b>job.file</b>
         *
         *
         *  @param job the job to process
         *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
         */
        bool
        ChapterUtility::actionEvery( JobContext &job )
        {
            ostringstream oss;
            oss << "Setting " << getChapterTypeName( _ChapterType ) << " chapters every "
                << _ChaptersEvery << " seconds in file " << '"' << job.file << '"' << endl;

            verbose1f( "%s", oss.str().c_str() );
            if( dryrunAbort() )
            {
                return SUCCESS;
            }

            job.fileHandle = MP4Modify( job.file.c_str() );
            if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
            {
                return herrf( "unable to open for write: %s\n", job.file.c_str() );
            }

            bool isVideoTrack = false;
            MP4TrackId refTrackId = getReferencingTrack( job.fileHandle, isVideoTrack );
            if( !MP4_IS_VALID_TRACK_ID(refTrackId) )
            {
                return herrf( "unable to find a video or audio track in file %s\n", job.file.c_str() );
            }

            Timecode refTrackDuration( MP4GetTrackDuration( job.fileHandle, refTrackId ), MP4GetTrackTimeScale( job.fileHandle, refTrackId ) );
            refTrackDuration.setScale( CHAPTERTIMESCALE );

            Timecode chapterDuration( _ChaptersEvery * 1000, CHAPTERTIMESCALE );
            chapterDuration.setFormat( Timecode::DECIMAL );
            vector<MP4Chapter_t> chapters;

            do
            {
                MP4Chapter_t chap;
                chap.duration = refTrackDuration.duration > chapterDuration.duration ? chapterDuration.duration : refTrackDuration.duration;
                sprintf(chap.title, "Chapter %lu", (unsigned long)chapters.size() + 1);

                chapters.push_back( chap );
                refTrackDuration -= chapterDuration;
            }
            while( refTrackDuration.duration > 0 );

            if( 0 < chapters.size() )
            {
                MP4SetChapters(job.fileHandle, &chapters[0], (uint32_t)chapters.size(), _ChapterType);
            }

            fixQtScale( job.fileHandle );
            job.optimizeApplicable = true;

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Action for exporting chapters from the <b>job.file</b>
         *
         *
         *  @param job the job to process
         *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
         */
        bool
        ChapterUtility::actionExport( JobContext &job )
        {
            job.fileHandle = MP4Read( job.file.c_str() );
            if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
            {
                return herrf( "unable to open for read: %s\n", job.file.c_str() );
            }

            // get the list of chapters
            MP4Chapter_t  *chapters = 0;
            uint32_t       chapterCount = 0;
            MP4ChapterType chtp = MP4GetChapters( job.fileHandle, &chapters, &chapterCount, _ChapterType );
            if (0 == chapterCount)
            {
                return herrf( "File \"%s\" does not contain chapters of type %s\n", job.file.c_str(),
                              getChapterTypeName( chtp ).c_str() );
            }

            // build the filename
            string outName = job.file;
            if( _ChapterFile.empty() )
            {
                FileSystem::pathnameStripExtension( outName );
                outName.append( ".chapters.txt" );
            }
            else
            {
                outName = _ChapterFile;
            }

            ostringstream oss;
            oss << "Exporting " << chapterCount << " " << getChapterTypeName( chtp );
            oss << " chapters from file " << '"' << job.file << '"' << " into chapter file " << '"' << outName << '"' << endl;

            verbose1f( "%s", oss.str().c_str() );
            if( dryrunAbort() )
            {
                // free up the memory
                MP4Free(chapters);

                return SUCCESS;
            }

            // open the file
            File out( outName, File::MODE_CREATE );
            if( openFileForWriting( out ) )
            {
                // free up the memory
                MP4Free(chapters);

                return FAILURE;
            }

            // write the chapters
#if defined( _WIN32 )
            static const char *LINEND = "\r\n";
#else
            static const char *LINEND = "\n";
#endif
            File::Size nout;
            bool failure = SUCCESS;
            int width = 2;
            if( CHPT_FMT_COMMON == _ChapterFormat && (chapterCount / 100) >= 1 )
            {
                width = 3;
            }
            Timecode duration( 0, CHAPTERTIMESCALE );
            duration.setFormat( Timecode::DECIMAL );
            for( uint32_t i = 0; i < chapterCount; ++i )
            {
                // print the infos
                ostringstream oss;
                switch( _ChapterFormat )
                {
                case CHPT_FMT_COMMON:
                    oss << "CHAPTER" << setw( width ) << setfill( '0' ) << i + 1 <<     '=' << duration.svalue << LINEND
                        << "CHAPTER" << setw( width ) << setfill( '0' ) << i + 1 << "NAME=" << chapters[i].title << LINEND;
                    break;
                case CHPT_FMT_NATIVE:
                default:
                    oss << duration.svalue << ' ' << chapters[i].title << LINEND;
                }

                string str = oss.str();
                if( out.write( str.c_str(), str.size(), nout ) )
                {
                    failure = herrf( "write to %s failed: %s\n", outName.c_str(), sys::getLastErrorStr() );
                    break;
                }

                // add the duration of this chapter to the sum (the start time of the next chapter)
                duration += Timecode(chapters[i].duration, CHAPTERTIMESCALE);
            }
            out.close();
            if( failure )
            {
                verbose1f( "removing file %s\n", outName.c_str() );
                ::remove( outName.c_str() );
            }

            // free up the memory
            MP4Free(chapters);

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Action for importing chapters into the <b>job.file</b>
         *
         *
         *  @param job the job to process
         *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
         */
        bool
        ChapterUtility::actionImport( JobContext &job )
        {
            vector<MP4Chapter_t> chapters;
            Timecode::Format format;

            // create the chapter file name
            string inName = job.file;
            if( _ChapterFile.empty() )
            {
                FileSystem::pathnameStripExtension( inName );
                inName.append( ".chapters.txt" );
            }
            else
            {
                inName = _ChapterFile;
            }

            if( parseChapterFile( inName, chapters, format ) )
            {
                return FAILURE;
            }

            ostringstream oss;
            oss << "Importing " << chapters.size() << " " << getChapterTypeName( _ChapterType );
            oss << " chapters from file " << inName << " into file " << '"' << job.file << '"' << endl;

            verbose1f( "%s", oss.str().c_str() );
            if( dryrunAbort() )
            {
                return SUCCESS;
            }

            if( 0 == chapters.size() )
            {
                return herrf( "No chapters found in file %s\n", inName.c_str() );
            }

            job.fileHandle = MP4Modify( job.file.c_str() );
            if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
            {
                return herrf( "unable to open for write: %s\n", job.file.c_str() );
            }

            bool isVideoTrack = false;
            MP4TrackId refTrackId = getReferencingTrack( job.fileHandle, isVideoTrack );
            if( !MP4_IS_VALID_TRACK_ID(refTrackId) )
            {
                return herrf( "unable to find a video or audio track in file %s\n", job.file.c_str() );
            }
            if( Timecode::FRAME == format && !isVideoTrack )
            {
                // we need a video track for this
                return herrf( "unable to find a video track in file %s but chapter file contains frame timestamps\n", job.file.c_str() );
            }

            // get duration and recalculate scale
            Timecode refTrackDuration( MP4GetTrackDuration( job.fileHandle, refTrackId ),
                                       MP4GetTrackTimeScale( job.fileHandle, refTrackId ) );
            refTrackDuration.setScale( CHAPTERTIMESCALE );

            // check for chapters starting after duration of reftrack
            for( vector<MP4Chapter_t>::iterator it = chapters.begin(); it != chapters.end(); )
            {
                Timecode curr( (*it).duration, CHAPTERTIMESCALE );
                if( refTrackDuration <= curr )
                {
                    hwarnf( "Chapter '%s' start: %s, playlength of file: %s, chapter cannot be set\n",
                            (*it).title, curr.svalue.c_str(), refTrackDuration.svalue.c_str() );
                    it = chapters.erase( it );
                }
                else
                {
                    ++it;
                }
            }
            if( 0 == chapters.size() )
            {
                return SUCCESS;
            }

            // convert start time into duration
            uint32_t framerate = static_cast<uint32_t>( CHAPTERTIMESCALE );
            if( Timecode::FRAME == format )
            {
                // get the framerate
                MP4SampleId sampleCount = MP4GetTrackNumberOfSamples( job.fileHandle, refTrackId );
                Timecode tmpcd( refTrackDuration.svalue, CHAPTERTIMESCALE );
                framerate = static_cast<uint32_t>( std::ceil( ((double)sampleCount / (double)tmpcd.duration) * CHAPTERTIMESCALE ) );
            }

            for( vector<MP4Chapter_t>::iterator it = chapters.begin(); it != chapters.end(); ++it )
            {
                MP4Duration currDur = (*it).duration;
                MP4Duration nextDur =  chapters.end() == it + 1 ? refTrackDuration.duration : (*(it + 1)).duration;

                if( Timecode::FRAME == format )
                {
                    // convert from frame nr to milliseconds
                    currDur = convertFrameToMillis( (*it).duration, framerate );

                    if( chapters.end() != it + 1 )
                    {
                        nextDur = convertFrameToMillis( (*(it + 1)).duration, framerate );
                    }
                }

                (*it).duration = nextDur - currDur;
            }

            // now set the chapters
            MP4SetChapters( job.fileHandle, &chapters[0], (uint32_t)chapters.size(), _ChapterType );

            fixQtScale( job.fileHandle );
            job.optimizeApplicable = true;

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Action for removing chapters from the <b>job.file</b>
         *
         *
         *  @param job the job to process
         *  @return mp4v2::util::SUCCESS if successful, mp4v2::util::FAILURE otherwise
         */
        bool
        ChapterUtility::actionRemove( JobContext &job )
        {
            ostringstream oss;
            oss << "Deleting " << getChapterTypeName( _ChapterType ) << " chapters from file " << '"' << job.file << '"' << endl;

            verbose1f( "%s", oss.str().c_str() );
            if( dryrunAbort() )
            {
                return SUCCESS;
            }

            job.fileHandle = MP4Modify( job.file.c_str() );
            if( job.fileHandle == MP4_INVALID_FILE_HANDLE )
            {
                return herrf( "unable to open for write: %s\n", job.file.c_str() );
            }

            MP4ChapterType chtp = MP4DeleteChapters( job.fileHandle, _ChapterType );
            if( MP4ChapterTypeNone == chtp )
            {
                return FAILURE;
            }

            fixQtScale( job.fileHandle );
            job.optimizeApplicable = true;

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** process positional argument
         *
         *  @see Utility::utility_job( JobContext& )
         */
        bool
        ChapterUtility::utility_job( JobContext &job )
        {
            if( !_action )
            {
                return herrf( "no action specified\n" );
            }

            return (this->*_action)( job );
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** process command-line option
         *
         *  @see Utility::utility_option( int, bool& )
         */
        bool
        ChapterUtility::utility_option( int code, bool &handled )
        {
            handled = true;

            switch( code )
            {
            case 'A':
            case LC_CHPT_ANY:
                _ChapterType = MP4ChapterTypeAny;
                break;

            case 'Q':
            case LC_CHPT_QT:
                _ChapterType = MP4ChapterTypeQt;
                break;

            case 'N':
            case LC_CHPT_NERO:
                _ChapterType = MP4ChapterTypeNero;
                break;

            case 'C':
            case LC_CHPT_COMMON:
                _ChapterFormat = CHPT_FMT_COMMON;
                break;

            case 'l':
            case LC_CHP_LIST:
                _action = &ChapterUtility::actionList;
                break;

            case 'e':
            case LC_CHP_EVERY:
            {
                istringstream iss( prog::optarg );
                iss >> _ChaptersEvery;
                if( iss.rdstate() != ios::eofbit )
                {
                    return herrf( "invalid number of seconds: %s\n", prog::optarg );
                }
                _action = &ChapterUtility::actionEvery;
                break;
            }

            case 'x':
                _action = &ChapterUtility::actionExport;
                break;

            case LC_CHP_EXPORT:
                _action = &ChapterUtility::actionExport;
                /* currently not supported since the chapters of n input files would be written to one chapter file
                _ChapterFile = prog::optarg;
                if( _ChapterFile.empty() )
                {
                    return herrf( "invalid TXT file: empty-string\n" );
                }
                */
                break;

            case 'i':
                _action = &ChapterUtility::actionImport;
                break;

            case LC_CHP_IMPORT:
                _action = &ChapterUtility::actionImport;
                /* currently not supported since the chapters of n input files would be read from one chapter file
                _ChapterFile = prog::optarg;
                if( _ChapterFile.empty() )
                {
                    return herrf( "invalid TXT file: empty-string\n" );
                }
                */
                break;

            case 'c':
            case LC_CHP_CONVERT:
                _action = &ChapterUtility::actionConvert;
                break;

            case 'r':
            case LC_CHP_REMOVE:
                _action = &ChapterUtility::actionRemove;
                break;

            default:
                handled = false;
                break;
            }

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Fix a QuickTime/iPod issue with long audio files.
         *
         *  This function checks if the <b>file</b> is a long audio file (more than
         *  about 6 1/2 hours) and modifies the timescale if necessary to allow
         *  playback of the file in QuickTime player and on some iPod models.
         *
         *  @param file the opened MP4 file
         */
        void
        ChapterUtility::fixQtScale(MP4FileHandle file)
        {
            // get around a QuickTime/iPod issue with storing the number of samples in a signed 32Bit value
            if( INT_MAX < MP4GetDuration(file))
            {
                bool isVideoTrack = false;
                if( MP4_IS_VALID_TRACK_ID(getReferencingTrack( file, isVideoTrack )) & isVideoTrack )
                {
                    // if it is a video, everything is different
                    return;
                }

                // timescale too high, lower it
                MP4ChangeMovieTimeScale(file, 1000);
            }
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Finds a suitable track that can reference a chapter track.
         *
         *  This function returns the first video or audio track that is found
         *  in the <b>file</b>.
         *  This track ca be used to reference the QuickTime chapter track.
         *
         *  @param file the opened MP4 file
         *  @param isVideoTrack receives true if the found track is video, false otherwise
         *  @return the <b>MP4TrackId</b> of the found track
         */
        MP4TrackId
        ChapterUtility::getReferencingTrack( MP4FileHandle file, bool &isVideoTrack )
        {
            isVideoTrack = false;

            uint32_t trackCount = MP4GetNumberOfTracks( file );
            if( 0 == trackCount )
            {
                return MP4_INVALID_TRACK_ID;
            }

            MP4TrackId refTrackId = MP4_INVALID_TRACK_ID;
            for( uint32_t i = 0; i < trackCount; ++i )
            {
                MP4TrackId    id = MP4FindTrackId( file, i );
                const char *type = MP4GetTrackType( file, id );
                if( MP4_IS_VIDEO_TRACK_TYPE( type ) )
                {
                    refTrackId = id;
                    isVideoTrack = true;
                    break;
                }
                else if( MP4_IS_AUDIO_TRACK_TYPE( type ) )
                {
                    refTrackId = id;
                    break;
                }
            }

            return refTrackId;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Return a human readable representation of a <b>MP4ChapterType</b>.
         *
         *  @param chapterType the chapter type
         *  @return a string representing the chapter type
         */
        string
        ChapterUtility::getChapterTypeName( MP4ChapterType chapterType) const
        {
            switch( chapterType )
            {
            case MP4ChapterTypeQt:
                return string( "QuickTime" );
                break;

            case MP4ChapterTypeNero:
                return string( "Nero" );
                break;

            case MP4ChapterTypeAny:
                return string( "QuickTime and Nero" );
                break;

            default:
                return string( "Unknown" );
            }
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Read a file into a buffer.
         *
         *  This function reads the file named by <b>filename</b> into a buffer allocated
         *  by malloc and returns the pointer to this buffer in <b>buffer</b> and the size
         *  of this buffer in <b>fileSize</b>.
         *
         *  @param filename  the name of the file.
         *  @param buffer    receives a pointer to the created buffer
         *  @param fileSize  reference to a <b>io::StdioFile::Size</b> that receives the size of the file
         *  @return true if there was an error, false otherwise
         */
        bool
        ChapterUtility::readChapterFile( const string filename, char **buffer, File::Size &fileSize )
        {
            // open the file
            File in( filename, File::MODE_READ );
            File::Size nin;
            if( in.open() )
            {
                return herrf( "opening chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() );
            }

            // get the file size
            fileSize = in.size;
            if( 0 >= fileSize )
            {
                in.close();
                return herrf( "getting size of chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() );
            }

            // allocate a buffer for the file and read the content
            char *inBuf = static_cast<char *>( malloc( fileSize + 1 ) );
            if( in.read( inBuf, fileSize, nin ) )
            {
                in.close();
                return herrf( "reading chapter file '%s' failed: %s\n", filename.c_str(), sys::getLastErrorStr() );
            }
            in.close();
            inBuf[fileSize] = 0;

            *buffer = inBuf;

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Read and parse a chapter file.
         *
         *  This function reads and parses a chapter file and returns a vector of
         *  <b>MP4Chapter_t</b> elements.
         *
         *  @param filename the name of the file.
         *  @param vector   receives a vector of chapters
         *  @param format   receives the <b>Timecode::Format</b> of the timestamps
         *  @return true if there was an error, false otherwise
         */
        bool
        ChapterUtility::parseChapterFile( const string filename, vector<MP4Chapter_t> &chapters, Timecode::Format &format )
        {
            // get the content
            char *inBuf;
            File::Size fileSize;
            if( readChapterFile( filename, &inBuf, fileSize ) )
            {
                return FAILURE;
            }

            // separate the text lines
            char *pos = inBuf;
            while (pos < inBuf + fileSize)
            {
                if (*pos == '\n' || *pos == '\r')
                {
                    *pos = 0;
                    if (pos > inBuf)
                    {
                        // remove trailing whitespace
                        char *tmp = pos - 1;
                        while ((*tmp == ' ' || *tmp == '\t') && tmp > inBuf)
                        {
                            *tmp = 0;
                            tmp--;
                        }
                    }
                }
                pos++;
            }
            pos = inBuf;

            // check for a BOM
            char bom[5] = {0};
            int bomLen = 0;
            const unsigned char *uPos = reinterpret_cast<unsigned char *>( pos );
            if( 0xEF == *uPos && 0xBB == *(uPos + 1) && 0xBF == *(uPos + 2) )
            {
                // UTF-8 (we do not need the BOM)
                pos += 3;
            }
            else if(   ( 0xFE == *uPos && 0xFF == *(uPos + 1) ) // UTF-16 big endian
                       || ( 0xFF == *uPos && 0xFE == *(uPos + 1) ) ) // UTF-16 little endian
            {
                // store the BOM to prepend the title strings
                bom[0] = *pos++;
                bom[1] = *pos++;
                bomLen = 2;
                return herrf( "chapter file '%s' has UTF-16 encoding which is not supported (only UTF-8 is allowed)\n",
                              filename.c_str() );
            }
            else if(   ( 0x0 == *uPos && 0x0 == *(uPos + 1) && 0xFE == *(uPos + 2) && 0xFF == *(uPos + 3) ) // UTF-32 big endian
                       || ( 0xFF == *uPos && *(uPos + 1) == 0xFE && *(uPos + 2) == 0x0 && 0x0 == *(uPos + 3) ) ) // UTF-32 little endian
            {
                // store the BOM to prepend the title strings
                bom[0] = *pos++;
                bom[1] = *pos++;
                bom[2] = *pos++;
                bom[3] = *pos++;
                bomLen = 4;
                return herrf( "chapter file '%s' has UTF-32 encoding which is not supported (only UTF-8 is allowed)\n",
                              filename.c_str() );
            }

            // parse the lines
            bool failure = false;
            uint32_t currentChapter = 0;
            FormatState formatState = FMT_STATE_INITIAL;
            char *titleStart = 0;
            uint32_t titleLen = 0;
            char *timeStart = 0;
            while( pos < inBuf + fileSize )
            {
                if( 0 == *pos || ' ' == *pos || '\t' == *pos )
                {
                    // uninteresting chars
                    pos++;
                    continue;
                }
                else if( '#' == *pos )
                {
                    // comment line
                    pos += strlen( pos );
                    continue;
                }
                else if( isdigit( *pos ) )
                {
                    // mp4chaps native format: hh:mm:ss.sss <title>

                    timeStart = pos;

                    // read the title if there is one
                    titleStart = strchr( timeStart, ' ' );
                    if( NULL == titleStart )
                    {
                        titleStart = strchr( timeStart, '\t' );
                    }

                    if( NULL != titleStart )
                    {
                        *titleStart = 0;
                        pos = ++titleStart;

                        while( ' ' == *titleStart || '\t' == *titleStart )
                        {
                            titleStart++;
                        }

                        titleLen = (uint32_t)strlen( titleStart );

                        // advance to the end of the line
                        pos = titleStart + 1 + titleLen;
                    }
                    else
                    {
                        // advance to the end of the line
                        pos += strlen( pos );
                    }

                    formatState = FMT_STATE_FINISH;
                }
#if defined( _MSC_VER )
                else if( 0 == strnicmp( pos, "CHAPTER", 7 ) )
#else
                else if( 0 == strncasecmp( pos, "CHAPTER", 7 ) )
#endif
                {
                    // common format: CHAPTERxx=hh:mm:ss.sss\nCHAPTERxxNAME=<title>

                    char *equalsPos = strchr( pos + 7, '=' );
                    if( NULL == equalsPos )
                    {
                        herrf( "Unable to parse line \"%s\"\n", pos );
                        failure = true;
                        break;
                    }

                    *equalsPos = 0;

                    char *tlwr = pos;
                    while( equalsPos != tlwr )
                    {
                        *tlwr = tolower( *tlwr );
                        tlwr++;
                    }

                    if( NULL != strstr( pos, "name" ) )
                    {
                        // mark the chapter title
                        uint32_t chNr = 0;
                        sscanf( pos, "chapter%dname", &chNr );
                        if( chNr != currentChapter )
                        {
                            // different chapter number => different chapter definition pair
                            if( FMT_STATE_INITIAL != formatState )
                            {
                                herrf( "Chapter lines are not consecutive before line \"%s\"\n", pos );
                                failure = true;
                                break;
                            }

                            currentChapter = chNr;
                        }
                        formatState = FMT_STATE_TIME_LINE == formatState ? FMT_STATE_FINISH
                                      : FMT_STATE_TITLE_LINE;

                        titleStart = equalsPos + 1;
                        titleLen = (uint32_t)strlen( titleStart );

                        // advance to the end of the line
                        pos = titleStart + titleLen;
                    }
                    else
                    {
                        // mark the chapter start time
                        uint32_t chNr = 0;
                        sscanf( pos, "chapter%d", &chNr );
                        if( chNr != currentChapter )
                        {
                            // different chapter number => different chapter definition pair
                            if( FMT_STATE_INITIAL != formatState )
                            {
                                herrf( "Chapter lines are not consecutive at line \"%s\"\n", pos );
                                failure = true;
                                break;
                            }

                            currentChapter = chNr;
                        }
                        formatState = FMT_STATE_TITLE_LINE == formatState ? FMT_STATE_FINISH
                                      : FMT_STATE_TIME_LINE;

                        timeStart = equalsPos + 1;

                        // advance to the end of the line
                        pos = timeStart + strlen( timeStart );
                    }
                }

                if( FMT_STATE_FINISH == formatState )
                {
                    // now we have title and start time
                    MP4Chapter_t chap;

                    strncpy( chap.title, titleStart, min( titleLen, (uint32_t)MP4V2_CHAPTER_TITLE_MAX ) );
                    chap.title[titleLen] = 0;

                    Timecode tc( 0, CHAPTERTIMESCALE );
                    string tm( timeStart );
                    if( tc.parse( tm ) )
                    {
                        herrf( "Unable to parse time code from \"%s\"\n", tm.c_str() );
                        failure = true;
                        break;
                    }
                    chap.duration = tc.duration;
                    format = tc.format;

                    // ad the chapter to the list
                    chapters.push_back( chap );

                    // re-initialize
                    formatState = FMT_STATE_INITIAL;
                    titleStart = timeStart = NULL;
                    titleLen = 0;
                }
            }
            free( inBuf );
            if( failure )
            {
                return failure;
            }

            return SUCCESS;
        }

        ///////////////////////////////////////////////////////////////////////////////

        /** Convert from frame to millisecond timestamp.
         *
         *  This function converts a timestamp from hh:mm:ss:ff to hh:mm:ss.sss
         *
         *  @param duration  the timestamp in hours:minutes:seconds:frames.
         *  @param framerate the frames per second
         *  @return the timestamp in milliseconds
         */
        MP4Duration
        ChapterUtility::convertFrameToMillis( MP4Duration duration, uint32_t framerate )
        {
            Timecode tc( duration, CHAPTERTIMESCALE );
            if( framerate < tc.subseconds )
            {
                uint64_t seconds = tc.subseconds / framerate;
                tc.setSeconds( tc.seconds + seconds );
                tc.setSubseconds( (tc.subseconds - (seconds * framerate)) * framerate );
            }
            else
            {
                tc.setSubseconds( tc.subseconds * framerate );
            }

            return tc.duration;
        }

        ///////////////////////////////////////////////////////////////////////////////

    }
} // namespace mp4v2::util

///////////////////////////////////////////////////////////////////////////////

extern "C"
int main( int argc, char **argv )
{
    mp4v2::util::ChapterUtility util( argc, argv );
    return util.process();
}
